/*
 * SIT_Menu.c : menu bar and popup menu management
 *
 * Written by T.Pierron, May 2009
 */

#include "SIT_P.h"
#include <ctype.h>
#include <stddef.h>
#include "graphics.h"

#undef	UTF8ToUTF16

static struct { STRPTR name; ULONG code; } keynames[] = {
	{"Home",      SITK_Home},
	{"End",       SITK_End},
	{"Shift",     SITK_FlagShift},
	{"Ctrl",      SITK_FlagCtrl},
	{"Alt",       SITK_FlagAlt},
	{"Ins",       SITK_Insert},
	{"Del",       SITK_Delete},
	{"Tab",       SITK_Tab},
	{"Esc",       SITK_Escape},
	{"Space",     SITK_Space},
	{"Help",      SITK_Help},
	{"Enter",     SITK_Return},
	{"Return",    SITK_Return},
	{"BackSpace", SITK_BackSpace}
	/* Other keys will have to be explicitely set */
};

/* Seems small, but having 800px wide menu isn't very user-friendly */
#define MAXMENULEN         80

/* Convert string into virtual key */
static int SIT_ParseAccel(STRPTR acc, ULONG code)
{
	int i, n;

	while (*acc)
	{
		if (isalpha(acc[0]) && acc[1] == 0)
		{
			code |= acc[0];
			break;
		}
		else if (acc[0] == 'F' && (n = atoi(acc+1)) > 0 && n <= 20)
		{
			code |= SITK_F1 + n - 1;
			break;
		}
		else for (i = 0; i < DIM(keynames); i ++)
		{
			STRPTR k = keynames[i].name;
			n = strlen(k);
			if (strncasecmp(k, acc, n) == 0 && strchr("+-", acc[n]))
			{
				code |= keynames[i].code;
				acc += n;
				if (*acc) acc ++;
				i = -1;
				break;
			}
		}
		if (i >= 0) break;
	}
	return code;
}

static int SIT_BuildMenuLabel(SIT_MenuStruct * menu, LPWSTR out, int max)
{
	LPWSTR wacc;
	STRPTR label = menu->label;
	STRPTR acc   = menu->shortcut;
	int    nbl   = utf16len(label) * sizeof *wacc;
	int    nba   = acc ? (utf16len(acc)+1) * sizeof *wacc : 0;

	if (IsDef(acc))
	{
		Bool add = isalpha(acc[0]) && acc[1] == 0;
		/* Convert to UTF-16 */
		if (add) nba += 10; /* Will add "Ctrl+" before */

		wacc = alloca(nba + sizeof *wacc);
		wcscpy(wacc, L"\t");
		if (add) wcscat(wacc, L"Ctrl+");
		UTF8ToUTF16((STRPTR) wcschr(wacc, 0), nba, acc, -1);
		if (menu->accel == 0)
			menu->accel = SIT_ParseAccel(acc, add ? SITK_FlagCtrl : 0);
	}
	else wacc = NULL;

	if (nbl > (max<<1) - nba)
	{
		/* Reduce label */
		LPWSTR src;
		LPWSTR full = alloca(nbl);
		int    nbm  = ((max<<1) - nba) / (2 * sizeof *out);

		MultiByteToWideChar(CP_UTF8, 0, label, -1, full, nbl / sizeof *full);
		src = full + nbm;

		/* Middle of a surrogate pair */
		if (0xD800 <= *src && *src <= 0xDBFF) src --;

		memcpy(out, full, src - full);
		out += src - full; *out++ = 0x2026;
		src = full + (nbl>>1) - nbm + 1;

		/* Middle of a surrogate pair */
		if (0xDC00 <= *src && *src <= 0xDFFF) src ++;
		wcscpy(out, src);
	}
	else UTF8ToUTF16((STRPTR) out, max, label, -1);

	if (wacc)
		wcscat(out, wacc);

	return wcslen(out);
}

/* Create image with its disabled state */
static Image SIT_MenuLoadImage(STRPTR format, int isImg)
{
	Bool  alt = False;
	Image img;

	if (! isImg && strncmp(format, "alt:", 4) == 0)
		format += 4, alt = True;

	if (isImg)
	{
		/* Always make a copy, we need a hbitmap anyway */
		Image src = (Image) format;
		img = GFX_CreateImage(src->width, src->height, src->bpp);
		memcpy(img->bitmap, src->bitmap, src->stride * src->height);
		GFX_ToNativeFormat(img);
	}
	else
	{
		STRPTR path = alloca(strlen(format) + 7);
		strcpy(path, "dib://");
		strcat(path, format);
		img = GFX_LoadImage(path);
	}
	return img;
}

APTR SIT_CreateMenu(SIT_Widget w, HMENU hmd, SIT_MenuStruct * menu, int pos)
{
	int lev  = menu->level;
	int slot = 0;
	int next = 0;

	while (menu->level >= lev)
	{
		MENUITEMINFO inf;
		WCHAR        label[MAXMENULEN]; /* Stupid to do more anyway */
		HMENU        sub = NULL;
		Image        img;

		if (menu[1].level > lev)
			sub = SIT_CreateMenu(w, CreateMenu(), menu + 1, 0);

		memset(&inf, 0, sizeof inf);

		if ((menu->flags & SITM_NoMenuEntry) == 0)
		{
			if (menu->flags >= 65536 && next != menu->menuid)
				slot = next = menu->menuid;

			inf.cbSize     = sizeof inf;
			inf.fMask      = MIIM_DATA | MIIM_ID | MIIM_STATE | MIIM_FTYPE | MIIM_STRING;
			inf.fType      = menu->label == SITM_SEPARATOR ? MFT_SEPARATOR : MFT_STRING;
			inf.fState     = 0;
			inf.wID        = menu->menuid;
			inf.hSubMenu   = sub;
			inf.dwItemData = slot;
			inf.dwTypeData = label;
			if (inf.fType != MFT_SEPARATOR)
				inf.cch = SIT_BuildMenuLabel(menu, label, DIM(label));

			if (sub) inf.fMask |= MIIM_SUBMENU;
			if (menu->flags & SITM_Checked)  inf.fState |= MFS_CHECKED;
			if (menu->flags & SITM_Disabled) inf.fState |= MFS_GRAYED;

			if (slot > 0)
			{
				inf.fType |= MFT_RADIOCHECK;
				next = menu->flags >> 16;
				if (menu->flags & SITM_Checked)
				{
					if (slot != menu->menuid)
					{
						MENUITEMINFO sub;
						memset(&sub, 0, sizeof sub);
						sub.cbSize     = sizeof sub;
						sub.fMask      = MIIM_DATA;
						sub.dwItemData = slot | (menu->menuid << 16);
						SetMenuItemInfo(hmd, slot, FALSE, &sub);
					}
					else inf.dwItemData |= menu->menuid << 16;
				}
				if (next == 0) slot = 0;
			}
			/* Do not allow bitmap on checkmark or radio entry */
			if (menu->flags & SITM_NoMenuIcon)
			{
				/* Be sure no icons will ever be added for this entry (especially from toolbars) */
				inf.dwItemData = -1;
			}
			else if (menu->bitmap && inf.fType != MFT_SEPARATOR && inf.dwItemData == 0 &&
			   (img = SIT_MenuLoadImage(menu->bitmap, menu->flags & SITM_GfxImage)))
			{
				/*
				 * Known bug: under WinXP and below (WINVER <= 0x0501), specifying an actual bitmap in
				 * hbmpItem field is next to unusable: weird placement and 32bpp bitmap are completely
				 * FUBAR. We'll handle the drawing of bitmap entirely by ourself. In later version, if
				 * we handle the drawing by ourself, menus are not themed. Of course, would be too easy.
				 */
				inf.fMask |= MIIM_BITMAP;
				inf.dwItemData = SIT_AddMenuIcon(w, inf.wID, NULL, NULL, 0, img);
				/* Note: hbmpChecked is drawn using AND blit. Completely useless for anything but B'n'W images */
				/* We will draw the bitmap ourself, it is not that hard */
				inf.hbmpItem = (win32vers > VER_WINXP && has_theme ? img->hbitmap : HBMMENU_CALLBACK);
			}
			InsertMenuItem(hmd, pos, True, &inf);
		}
		else if (menu->accel == 0)
		{
			/* Only want accelerator */
			SIT_BuildMenuLabel(menu, label, DIM(label));
		}
		do menu ++; while (menu->level > lev);
		pos ++;
	}
	return hmd;
}

/* Handle toggle menu and auto-checked */
void SIT_HandleAutoMenu(SIT_Widget w, int menuid)
{
	MENUITEMINFO inf;
	SIT_Dialog d = (SIT_Dialog) w;

	memset(&inf, 0, sizeof inf);
	inf.cbSize = sizeof inf;
	inf.fMask  = MIIM_DATA;

	if (GetMenuItemInfo(d->sd_MenuBar, menuid, FALSE, &inf) && inf.dwItemData > 0)
	{
		int old, id = inf.dwItemData & 65535;

		if (id != menuid)
			/* Stored into another slot */
			GetMenuItemInfo(d->sd_MenuBar, id, FALSE, &inf);

		/* dwItemData now contains item currently selected in group */
		if ((old = inf.dwItemData >> 16) != menuid)
		{
			/* Deselect old (dwItemData), select new (menuid), update master */
			inf.dwItemData = id | (menuid << 16);
			SetMenuItemInfo(d->sd_MenuBar, id, FALSE, &inf);
			inf.fState = 0;
			inf.fMask  = MIIM_STATE;
			SetMenuItemInfo(d->sd_MenuBar, old, FALSE, &inf);
			inf.fState = MFS_CHECKED;
			SetMenuItemInfo(d->sd_MenuBar, menuid, FALSE, &inf);
		}
	}
}

#define d ((SIT_Dialog)w)
DLLIMP void SIT_CheckMenu(SIT_Widget w, int menuid, Bool checked, Bool doCB)
{
	MENUITEMINFO inf = {.cbSize = sizeof inf, .fMask = MIIM_STATE};
	while (w && w->sw_Type != SIT_DIALOG) w = w->sw_Parent;
	if (w == NULL) return;
	Bool sync = True;
	if (menuid < 0) menuid = -menuid, sync = False;

	if (checked) inf.fState = MFS_CHECKED;
	SetMenuItemInfo(d->sd_MenuBar, menuid, FALSE, &inf);
	inf.fMask = MIIM_DATA;

	if (GetMenuItemInfo(d->sd_MenuBar, menuid, FALSE, &inf) && inf.dwItemData > 0)
		SIT_HandleAutoMenu(w, menuid);

	if (sync)
		for (w = HEAD(d->sd_SyncTB); w; NEXT(w))
			SIT_SetToolButton(&TOOLBAR(w)->super, -menuid, SITTB_FlagSet(SITTB_Checked, checked>0), DontChangePtr, DontChangePtr);

	if (doCB)
		SIT_ApplyCallback(&d->super, (APTR) menuid, SITE_OnMenu);
}

DLLIMP void SIT_EnableMenu(SIT_Widget w, int menuid, Bool ena)
{
	MENUITEMINFO inf = {.cbSize = sizeof inf, .fMask = MIIM_STATE};
	while (w && w->sw_Type != SIT_DIALOG) w = w->sw_Parent;
	if (w == NULL) return;
	Bool sync = True;
	if (menuid < 0) menuid = -menuid, sync = False;

	if (GetMenuItemInfo(d->sd_MenuBar, menuid, FALSE, &inf))
	{
		if (ena) inf.fState &= ~MFS_GRAYED,  inf.fState |= MFS_ENABLED;
		else     inf.fState &= ~MFS_ENABLED, inf.fState |= MFS_GRAYED;

		SetMenuItemInfo(d->sd_MenuBar, menuid, FALSE, &inf);

		if (sync)
			for (w = HEAD(d->sd_SyncTB); w; NEXT(w))
				SIT_SetToolButton(&TOOLBAR(w)->super, menuid, SITTB_FlagSet(SITTB_Disabled, !ena), DontChangePtr, DontChangePtr);
	}
}

DLLIMP int SIT_GetMenuState(SIT_Widget w, int menuid)
{
	MENUITEMINFO inf = {.cbSize = sizeof inf, .fMask = MIIM_STATE};
	while (w && w->sw_Type != SIT_DIALOG) w = w->sw_Parent;
	if (w == NULL) return -1;
	if (GetMenuItemInfo(d->sd_MenuBar, menuid, FALSE, &inf))
	{
		int ret = 0;
		if (inf.fState & MFS_GRAYED) ret = SITM_Disabled;
		if (inf.fState & MFS_CHECKED) ret |= SITM_Checked;
		return ret;
	}
	TBBUTTONINFO info = {.cbSize = sizeof info, .dwMask = TBIF_STATE};
	/* Menu not found, check if there is a toolbar instead */
	for (w = HEAD(d->sd_SyncTB); w; NEXT(w))
	{
		if (SendMessage(TOOLBAR(w)->tb_Handle, TB_GETBUTTONINFO, menuid, (LPARAM) &info) >= 0)
		{
			int ret = 0;
			if ((info.fsState & TBSTATE_ENABLED) == 0) ret = SITM_Disabled;
			if (info.fsState & TBSTATE_CHECKED) ret |= SITM_Checked;
			return ret;
		}
	}
	return -1;
}

#undef d

void SIT2Accel(SIT_MenuStruct * menu, LPACCEL a)
{
	ULONG   k = menu->accel & ~(SITK_FlagAlt | SITK_FlagCtrl | SITK_FlagShift);
	memset(a, 0, sizeof *a);
	a->fVirt = FVIRTKEY;
	if (menu->accel & SITK_FlagAlt)   a->fVirt |= FALT;
	if (menu->accel & SITK_FlagCtrl)  a->fVirt |= FCONTROL;
	if (menu->accel & SITK_FlagShift) a->fVirt |= FSHIFT;
	a->cmd = menu->menuid;
	a->key = k <= 127 ? k : sit2vk[k - SITK_Home];
}

typedef struct MenuAccel_t   MenuAccel;

struct MenuAccel_t
{
	LPACCEL accel;
	int     count, max;
};

#define	dlg        ((SIT_Dialog)w)
#define ROUND(x)   (((x)+7) & ~7)

static void SIT_DeleteMenu(SIT_Widget w, MenuAccel * ma, int cmdid)
{
	MENUITEMINFO mi = {.cbSize = sizeof mi, .fMask = MIIM_SUBMENU | MIIM_FTYPE | MIIM_DATA};
	LPACCEL accel = ma->accel;

	if (GetMenuItemInfo(dlg->sd_MenuBar, cmdid, False, &mi))
	{
		int i, nb;
		/* Check if accel table has to be recomputed */
		for (i = 0, nb = ma->count; i < nb; i ++) {
			if (accel[i].cmd == cmdid) {
				memmove(accel+i, accel+i+1, (nb - i - 1) * sizeof *accel); ma->count--;
				w->sw_Flags |= SITF_Style1Changed;
				break;
			}
		}
		if (mi.dwItemData > 0 && mi.dwItemData != -1 && !(mi.fType & MFT_RADIOCHECK))
			SIT_DelMenuIcon(w, mi.dwItemData - 1);
		/* Need to recurse for accel :-( */
		if (mi.hSubMenu)
		{
			HMENU sub = mi.hSubMenu;
			mi.fMask = MIIM_ID;
			for (nb = GetMenuItemCount(sub), i = 0; i < nb; i ++)
				if (GetMenuItemInfo(sub, 0, True, &mi)) SIT_DeleteMenu(w, ma, mi.wID);
		}
		DeleteMenu(dlg->sd_MenuBar, cmdid, MF_BYCOMMAND);
	}
}

static void SIT_AddOrReplaceAccel(MenuAccel * ma, SIT_MenuStruct * item)
{
	int i, nb;
	for (i = 0, nb = ma->count; i < nb && ma->accel[i].cmd != item->menuid; i ++);

	if (i == nb)
	{
		/* Add a new one */
		if (i == ma->max) {
			ma->max   = ROUND(ma->max+1);
			ma->accel = realloc(ma->accel, sizeof *ma->accel * ma->max);
		}
		SIT2Accel(item, ma->accel + i);
		ma->count ++;
	}
	else SIT2Accel(item, ma->accel + i); /* Overwrite existing accel */
}

static void SIT_EditMenu(SIT_Widget w, SIT_MenuStruct * item, MenuAccel * ma)
{
	WCHAR label[128];
	WCHAR buffer[MAXMENULEN];
	LPWSTR sep;
	int flags = 0;
	MENUITEMINFO inf = {.cbSize = sizeof inf, .fMask = MIIM_DATA | MIIM_ID | MIIM_STATE | MIIM_TYPE, .wID = item->menuid,
		.dwTypeData = label, .cch = DIM(label)};

	if (GetMenuItemInfo(dlg->sd_MenuBar, inf.wID, False, &inf))
	{
		/* Menu does exists, modify it */
		inf.fType      = item->label == SITM_SEPARATOR ? MFT_SEPARATOR : MFT_STRING;
		inf.dwItemData = item->flags >> 16;

		if (item->flags & SITM_Checked)  inf.fState |= MFS_CHECKED;
		if (item->flags & SITM_Disabled) inf.fState |= MFS_GRAYED;

		if (item->label == DontChangePtr)
			item->label = "x", flags = 1;
		if (item->shortcut == DontChangePtr)
			item->shortcut = NULL, flags |= 2;

		if (inf.fType != MFT_SEPARATOR)
			SIT_BuildMenuLabel(item, buffer, DIM(buffer));

		switch (flags) {
		case 1: // Keep existing label, replace shortcut, if any
			sep = wcschr(label, '\t');
			if (sep) *sep = 0;
			sep = wcschr(buffer, '\t');
			if (sep) wcscat(label, sep);
			break;
		case 2: // Replace label, keep shortcut
			sep = wcschr(buffer, '\t');
			if (sep) *sep = 0;
			sep = wcschr(label, '\t');
			if (sep) wcscat(buffer, sep);
			// no break;
		case 0: // Replace both
			inf.dwTypeData = buffer;
		}
		if (flags & 2)
		{
			SIT_AddOrReplaceAccel(ma, item);
			w->sw_Flags |= SITF_Style1Changed;
		}
		SetMenuItemInfo(dlg->sd_MenuBar, inf.wID, False, &inf);
	}
}

static void SIT_AddMenu(SIT_Widget w, SIT_MenuStruct * item, MenuAccel * ma, int id, int pos)
{
	MENUITEMINFO info = {.cbSize = sizeof info, .fMask = MIIM_SUBMENU};

	if (GetMenuItemInfo(dlg->sd_MenuBar, id, FALSE, &info) && info.hSubMenu)
	{
		SIT_MenuStruct * m;
		int nb = GetMenuItemCount(info.hSubMenu);
		if (pos < 0) pos = nb;
		SIT_CreateMenu(w, info.hSubMenu, item, pos);

		/* Modify accel table */
		for (m = item; m->level; m ++)
		{
			if (m->accel > 0) SIT_AddOrReplaceAccel(ma, m), w->sw_Flags |= SITF_Style1Changed;
		}
	}
}

DLLIMP void SIT_AlterMenu(SIT_Widget w, ...)
{
	MenuAccel ma = {};
	va_list   args;
	int       action;

	while (w && w->sw_Type != SIT_DIALOG)
		w = w->sw_Parent;

	if (dlg->sd_Accel)
	{
		ma.count = CopyAcceleratorTable(dlg->sd_Accel, NULL, 0);
		ma.max   = ROUND(ma.count);
		ma.accel = malloc(sizeof *ma.accel * ma.max);
		CopyAcceleratorTable(dlg->sd_Accel, ma.accel, ma.count);
	}

	va_start(args, w);
	while ((action = va_arg(args, int)))
	{
		switch (action) {
		case SITM_Delete:
			SIT_DeleteMenu(w, &ma, va_arg(args, int));
			break;
		case SITM_Append:
		{	int id  = va_arg(args, int);
			int pos = va_arg(args, int);
			SIT_AddMenu(w, va_arg(args, APTR), &ma, id, pos);
		}	break;
		case SITM_Edit:
			SIT_EditMenu(w, va_arg(args, APTR), &ma);
			break;
		}
	}
	va_end(args);
	if (w->sw_Flags & SITF_Style1Changed)
	{
		DestroyAcceleratorTable(dlg->sd_Accel);
		dlg->sd_Accel = ma.count > 0 ? CreateAcceleratorTable(ma.accel, ma.count) : NULL;
		w->sw_Flags &= ~SITF_Style1Changed;
	}
	if (ma.accel) free(ma.accel);
}
#undef ROUND
#undef dlg

/* Display a popup menu and return the ID of selected item (0 if none) */
DLLIMP int SIT_PopupMenu(SIT_Widget w, SIT_MenuStruct * menu, int x, int y, SIT_Widget relto)
{
	HMENU hmenu = SIT_CreateMenu(w, CreatePopupMenu(), menu, 0);
	POINT point = {.x = x, .y = y};
	int   flags = TPM_LEFTALIGN | TPM_TOPALIGN;

	if (relto)
	{
		flags = 0;
		int w = relto->sw_Box.width  - relto->sw_Box.x;
		int h = relto->sw_Box.height - relto->sw_Box.y;
		switch (x) {
		case SITV_AlignLeft:   flags = TPM_RIGHTALIGN;  x = 0; break;
		case SITV_AlignCenter: flags = TPM_CENTERALIGN; x = w/2; break;
		case SITV_AlignRight:  flags = TPM_LEFTALIGN;   x = w+1; break;
		default:               flags = TPM_LEFTALIGN;
		}
		switch (y) {
		case SITV_AlignTop:    flags |= TPM_BOTTOMALIGN;  y = 0; break;
		case SITV_AlignCenter: flags |= TPM_VCENTERALIGN; y = h/2; break;
		case SITV_AlignBottom: flags |= TPM_TOPALIGN;     y = h+2; break;
		default:               flags |= TPM_TOPALIGN;
		}
		point.x = x;
		point.y = y;
		ClientToScreen(relto->sw_Handle, &point);
	}
	else if (x < 0 || y < 0)
	{
		point.x = point.y = 0;
		ClientToScreen(w->sw_Handle, &point);
	}

	int cmd = TrackPopupMenuEx(GetSubMenu(hmenu, 0), flags | TPM_NONOTIFY |
		TPM_RETURNCMD, point.x, point.y, w->sw_Handle, NULL);

	DestroyMenu(hmenu);

	return cmd;
}



